﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using nRoute.Components.Collections;
using nRoute.Components.Composition.Adapters;
using nRoute.Components.Messaging;
using nRoute.Utilities;
#if (WINRT)
#endif
#if (!SILVERLIGHT)
using System.Collections.Concurrent;
using System.Threading.Tasks;
#endif

namespace nRoute.Components.Composition
{
    public sealed partial class ResourceCatalog
        : IServiceProvider, IDisposable, IResourceCatalog, IResourceCatalogEx
    {
        
#region Declarations

        private const string CANNOT_RESOLVE_RESOURCE = "Cannot resolve resource of type {0}";
        private const string CANNOT_RESOLVE_LOCATOR = "Cannot resolve locator for type {0}";
        private const int INITIAL_CAPACITY = 251;
        private const int CONCCURRENCY_LEVEL_FACTOR = 2;

        private readonly Object _lock = new Object();
        private readonly CompositeDisposable _compositeDisposable;
        private readonly ConcurrentDictionary<Type, IResourceLocator> _defaultResourceLocators;
        private readonly ConcurrentDictionary<ResourceKey, IResourceLocator> _resourceLocators;
        private readonly ConcurrentDictionary<Func<Type, bool>, Func<Type, ILocatorAdapter>> _resourceAdapters;
        private readonly ConcurrentOrderedList<Type> _knownTypes;
        private readonly ConcurrentOrderedList<IIntializableResource> _pendingIntializableResources;
        private readonly ConcurrentDictionary<Type, ISubject<CatalogChange>> _catalogListerners;
        private readonly ITypeBuilder _typeBuilder;
        private readonly RuntimeMapper _runtimeMapper;

#endregion

        public ResourceCatalog()
        {
            _compositeDisposable = new CompositeDisposable(INITIAL_CAPACITY);
            _defaultResourceLocators = new ConcurrentDictionary<Type, IResourceLocator>(Compatability.PROC_COUNT * CONCCURRENCY_LEVEL_FACTOR, INITIAL_CAPACITY);
            _resourceLocators = new ConcurrentDictionary<ResourceKey, IResourceLocator>(Compatability.PROC_COUNT * CONCCURRENCY_LEVEL_FACTOR, INITIAL_CAPACITY, ResourceKeyComparer.Default);
            _catalogListerners = new ConcurrentDictionary<Type, ISubject<CatalogChange>>(Compatability.PROC_COUNT * CONCCURRENCY_LEVEL_FACTOR, INITIAL_CAPACITY);
            _resourceAdapters = new ConcurrentDictionary<Func<Type, bool>, Func<Type, ILocatorAdapter>>(Compatability.PROC_COUNT * CONCCURRENCY_LEVEL_FACTOR, INITIAL_CAPACITY);
            _pendingIntializableResources = new ConcurrentOrderedList<IIntializableResource>();
            _knownTypes = new ConcurrentOrderedList<Type>();

            _typeBuilder = new TypeBuilder(this);
            _runtimeMapper = new RuntimeMapper(this);
        }

#region Initializers

#if (WINRT)
        public async Task Initialize()
        {
            RegisterSelfResources();
            await _runtimeMapper.Initialize();
        }
#else
        public void Initialize(Action completedCallback)
        {
            RegisterSelfResources();
            _runtimeMapper.Initialize(completedCallback);
        }
#endif

#endregion

#region IServiceProvider Related

        object IServiceProvider.GetService(Type serviceType)
        {
            return GetResource(serviceType);
        }

#endregion

#region IResourceCatalog Related

        public object GetResource(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return this.GetResource<Object>(resourceType, null);
        }

        public T GetResource<T>()
        {
            return this.GetResource<T>(typeof(T), null);
        }

        public object GetResource(Type resourceType, string key)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");

            return this.GetResource<Object>(resourceType, key);
        }

        public T GetResource<T>(string key)
        {
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");
            return this.GetResource<T>(typeof(T), key);
        }

        public bool IsResourceable(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return IsResourceServicable(resourceType, null);
        }

        public bool IsResourceable<T>()
        {
            return IsResourceServicable(typeof(T), null);
        }

        public bool IsResourceable(Type resourceType, string key)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");

            return IsResourceServicable(resourceType, key);
        }

        public bool IsResourceable<T>(string key)
        {
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");
            return IsResourceServicable(typeof(T), key);
        }

        public bool TryGetResource(Type resourceType, out object resourceInstance)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return TryGetResource<Object>(resourceType, null, out resourceInstance);
        }

        public bool TryGetResource<T>(out T resourceInstance)
        {
            return TryGetResource<T>(typeof(T), null, out resourceInstance);
        }

        public bool TryGetResource(Type resourceType, string key, out object resourceInstance)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");

            return TryGetResource<Object>(resourceType, key, out resourceInstance);
        }

        public bool TryGetResource<T>(string key, out T resourceInstance)
        {
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");
            return TryGetResource<T>(typeof(T), key, out resourceInstance);
        }

        public IEnumerable<Object> GetAllResources(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return GetAllResources<Object>(resourceType);
        }

        public IEnumerable<T> GetAllResources<T>()
        {
            return GetAllResources<T>(typeof(T));
        }

#endregion

#region IResourceCatalogEx Related

        public IResourceLocator GetResourceLocator(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return GetLocator(resourceType, null);
        }

        public IResourceLocator GetResourceLocator<T>()
        {
            return GetLocator(typeof(T), null);
        }

        public IResourceLocator GetResourceLocator(Type resourceType, string key)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");

            return GetLocator(resourceType, key);
        }

        public IResourceLocator GetResourceLocator<T>(string key)
        {
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");
            return GetLocator(typeof(T), key);
        }

        public bool TryGetResourceLocator(Type resourceType, out IResourceLocator resourceLocator)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return TryGetLocator(resourceType, out resourceLocator);
        }

        public bool TryGetResourceLocator<T>(out IResourceLocator resourceLocator)
        {
            return TryGetResourceLocator(typeof(T), out resourceLocator);
        }

        public bool TryGetResourceLocator(Type resourceType, string key, out IResourceLocator resourceLocator)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");

            return TryGetLocator(resourceType, key, out resourceLocator);
        }

        public bool TryGetResourceLocator<T>(string key, out IResourceLocator resourceLocator)
        {
            return TryGetResourceLocator(typeof(T), key, out resourceLocator);
        }

        public IEnumerable<IResourceLocator> GetResourceCatalog(Type resourceType)
        {
            return _resourceLocators.Where((kv) => kv.Key.Type == resourceType).Select((kv) => kv.Value).ToArray();
        }

        public IEnumerable<IResourceLocator> GetResourceCatalog<T>()
        {
            return GetResourceCatalog(typeof(T));
        }

        public bool IsKnownType(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return _knownTypes.Contains(resourceType);
        }

        public bool IsKnownType<T>()
        {
            return IsKnownType(typeof(T));
        }

        public void CatalogAsKnownType(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            _knownTypes.TryAdd(resourceType);
        }

        public void CatalogAsKnownType<T>()
        {
            CatalogAsKnownType(typeof(T));
        }

        public bool CatalogResource(Type resourceType, IResourceLocator resourceLocator)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNull(resourceLocator, "resourceLocator");

            return RegisterResource(resourceType, null, resourceLocator, null);
        }

        public bool CatalogResource<T>(IResourceLocator resourceLocator)
        {
            return CatalogResource(typeof(T), resourceLocator);
        }

        public bool CatalogResource(Type resourceType, string key, IResourceLocator resourceLocator, bool defaultResource)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNull(resourceLocator, "resourceLocator");

            return RegisterResource(resourceType, key, resourceLocator, defaultResource);
        }

        public bool CatalogResource<T>(string key, IResourceLocator resourceLocator, bool defaultResource)
        {
            return CatalogResource(typeof(T), key, resourceLocator, defaultResource);
        }

        public bool CatalogDefaultResourceKey(Type resourceType, string key)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNull(key, "key");

            return RegisterAsDefaultResource(resourceType, key);
        }

        public bool CatalogDefaultResourceKey<T>(string key)
        {
            return CatalogDefaultResourceKey(typeof(T), key);
        }

        /// <remarks>
        /// Note when removing item, you need to specify the key - as a null key will not work.
        /// </remarks>
        public bool UncatalogResource(Type resourceType, string key)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            Guard.ArgumentNotNullOrWhiteSpace(key, "key");          

            return UnRegisterResource(resourceType, key);
        }

        public bool UncatalogResource<T>(string key)
        {
            return UnRegisterResource(typeof(T), key);
        }

        public bool RegisterAdapter(Func<Type, bool> adapterPredicate, Func<Type, ILocatorAdapter> adapterFactory)
        {
            Guard.ArgumentNotNull(adapterPredicate, "adapterPredicate");
            Guard.ArgumentNotNull(adapterFactory, "adapterFactory");
            return _resourceAdapters.TryAdd(adapterPredicate, adapterFactory);
        }

        public bool UnregisterAdapter(Func<Type, bool> adapterPredicate)
        {
            Guard.ArgumentNotNull(adapterPredicate, "adapterPredicate");

            var _adapterFactory = default(Func<Type, ILocatorAdapter>);
            return _resourceAdapters.TryRemove(adapterPredicate, out _adapterFactory);
        }

        public IObservable<CatalogChange> GetCatalogPublisher(Type resourceType)
        {
            Guard.ArgumentNotNull(resourceType, "resourceType");
            return GetOrCreateCatalogPublisher(resourceType);
        }

        public ITypeBuilder TypeBuilder
        {
            get { return _typeBuilder; }
        }

#endregion

#region Cataloging Related

        private IEnumerable<TResult> GetAllResources<TResult>(Type resourceType)
        {
            return _resourceLocators.Where((kv) => kv.Key.Type == resourceType).Select((kv) => (TResult)kv.Value.GetResource(_typeBuilder)).ToArray();
        }

        private TResult GetResource<TResult>(Type resourceType, string key)
        {
            var _innerEx = default(Exception);

            try
            {
                if (_knownTypes.Contains(resourceType))
                {
                    if (string.IsNullOrWhiteSpace(key))
                    {
                        return (TResult)_defaultResourceLocators[resourceType].GetResource(_typeBuilder);
                    }
                    else
                    {
                        return (TResult)_resourceLocators[new ResourceKey(resourceType, key)].GetResource(_typeBuilder);
                    }
                }

                foreach (var _adapterPredicate in _resourceAdapters.Keys)
                {
                    if (_adapterPredicate(resourceType))
                    {
                        return (TResult)_resourceAdapters[_adapterPredicate](resourceType).Resolve(_typeBuilder, key);
                    }
                }
            }
            catch (Exception ex)
            {
                _innerEx = ex;
            }

            throw new ResolveResourceException(string.Format(CANNOT_RESOLVE_RESOURCE, resourceType.FullName), resourceType, _innerEx);
        }

        private bool TryGetResource<TResult>(Type resourceType, string key, out TResult resourceInstance)
        {
            var _resourceLocator = default(IResourceLocator);

            if (string.IsNullOrWhiteSpace(key))
            {
                if (_defaultResourceLocators.TryGetValue(resourceType, out _resourceLocator))
                {
                    resourceInstance = (TResult)_resourceLocator.GetResource(_typeBuilder);
                    return true;
                }
            }
            else
            {
                var _resourceKey = new ResourceKey(resourceType, key);
                if (_resourceLocators.TryGetValue(_resourceKey, out _resourceLocator))
                {
                    resourceInstance = (TResult)_resourceLocator.GetResource(_typeBuilder);
                    return true;
                }
            }

            foreach (var _adapterPredicate in _resourceAdapters.Keys)
            {
                if (_adapterPredicate(resourceType))
                {
                    resourceInstance = (TResult)_resourceAdapters[_adapterPredicate](resourceType).Resolve(_typeBuilder, null);
                    return true;
                }
            }

            resourceInstance = default(TResult);
            return false;
        }

        private bool IsResourceServicable(Type resourceType, string key)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                if (_defaultResourceLocators.ContainsKey(resourceType)) return true;
            }
            else
            {
                if (_resourceLocators.ContainsKey(new ResourceKey(resourceType, key))) return true;
            }

            foreach (var _adapterPredicate in _resourceAdapters.Keys)
            {
                if (_adapterPredicate(resourceType)) return true;
            }

            return false;
        }

        private IResourceLocator GetLocator(Type resourceType, string key)
        {
            var _innerEx = default(Exception);

            try
            {
                if (string.IsNullOrWhiteSpace(key))
                {
                    return _defaultResourceLocators[resourceType];
                }
                else
                {
                    return _resourceLocators[new ResourceKey(resourceType, key)];
                }
            }
            catch (Exception ex)
            {
                _innerEx = ex;
            }

            throw new ResolveResourceException(string.Format(CANNOT_RESOLVE_LOCATOR, resourceType.FullName), resourceType, _innerEx);
        }

        private bool TryGetLocator(Type resourceType, out IResourceLocator resourceLocator)
        {
            return _defaultResourceLocators.TryGetValue(resourceType, out resourceLocator);
        }

        private bool TryGetLocator(Type resourceType, string key, out IResourceLocator resourceLocator)
        {
            var _resourceKey = new ResourceKey(resourceType, key);
            return _resourceLocators.TryGetValue(_resourceKey, out resourceLocator);
        }

        private bool RegisterResource(Type resourceType, string key, IResourceLocator resourceLocator, bool? defaultResource)
        {
            // locks to ensure concurrency across multiple collections
            lock (_lock)
            {
                var _resourceKey = new ResourceKey(resourceType, key);
                if (_resourceLocators.TryAdd(_resourceKey, resourceLocator))
                {
                    if (defaultResource.GetValueOrDefault(false) || !_defaultResourceLocators.ContainsKey(resourceType))
                    {
                        _defaultResourceLocators.AddOrUpdate(resourceType, resourceLocator, (t, l) => resourceLocator);
                        _knownTypes.TryAdd(resourceType);                                   // could already be there
                    }

                    if (typeof(IIntializableResource).IsAssignableFrom(resourceLocator.GetType()))
                    {
                        ProcessIntializableResource((IIntializableResource)resourceLocator);
                    }

                    ProcessCatalogChange(resourceType, resourceLocator, CatalogChangeAction.Add);
                    _compositeDisposable.Add(resourceLocator);

                    return true;
                }
            }

            return false;
        }

        private bool UnRegisterResource(Type resourceType, string key)
        {
            // locks to ensure concurrency across multiple collections
            lock (_lock)
            {
                var _resourceKey = new ResourceKey(resourceType, key);
                var _resourceLocator = default(IResourceLocator);

                if (_resourceLocators.TryRemove(_resourceKey, out _resourceLocator))
                {
                    if (_defaultResourceLocators == _resourceLocator)
                    {
                        var _alternativeResourceLocator = _resourceLocators.FirstOrDefault((kv) => kv.Key.Type == resourceType);
                        if (_alternativeResourceLocator.Value != null)
                        {
                            if (!_defaultResourceLocators.TryUpdate(resourceType, _alternativeResourceLocator.Value, _resourceLocator))
                            {
                                _defaultResourceLocators.TryRemove(resourceType, out _resourceLocator);
                            }
                        }
                        else
                        {
                            _defaultResourceLocators.TryRemove(resourceType, out _resourceLocator);
                        }
                    }

                    ProcessCatalogChange(resourceType, _resourceLocator, CatalogChangeAction.Remove);
                    _compositeDisposable.Remove(_resourceLocator);
                    if (!_resourceLocator.IsDisposed) _resourceLocator.Dispose();

                    return true;
                }
            }

            return false;
        }

        private bool RegisterAsDefaultResource(Type resourceType, string key)
        {
            lock (_lock)
            {
                var _resourceKey = new ResourceKey(resourceType, key);
                var _resourceLocator = default(IResourceLocator);

                if (_resourceLocators.TryGetValue(_resourceKey, out _resourceLocator))
                {
                    _defaultResourceLocators.AddOrUpdate(resourceType, _resourceLocator, (t, l) => _resourceLocator);
                    return true;
                }
            }

            return false;
        }

        private void ProcessIntializableResource(IIntializableResource resource)
        {
            if (resource.CanInitialize(_typeBuilder))
            {
                resource.Initialize(_typeBuilder);
            }
            else
            {
                _pendingIntializableResources.TryAdd(resource);
            }

            if (_pendingIntializableResources.Count > 0)
            {
                var _resourceInitializedFlag = false;
                while (true)
                {
                    var _pendingInitializers = _pendingIntializableResources.ToArray();
                    foreach (var _pendingInitialzer in _pendingInitializers)
                    {
                        if (((ICancelable)_pendingInitialzer).IsDisposed)
                        {
                            _pendingIntializableResources.TryRemove(_pendingInitialzer);
                        }
                        else if (_pendingInitialzer.CanInitialize(_typeBuilder))
                        {
                            if (_pendingIntializableResources.TryRemove(_pendingInitialzer))
                            {
                                _pendingInitialzer.Initialize(_typeBuilder);
                            }
                            _resourceInitializedFlag = true;
                        }
                    }

                    // short-circuit
                    if (!_resourceInitializedFlag) break;
                    _resourceInitializedFlag = false;
                }
            }
        }

        private void ProcessCatalogChange(Type resourceType, IResourceLocator resourceLocator, CatalogChangeAction changeAction)
        {
            var _publisher = default(ISubject<CatalogChange>);
            if (_catalogListerners.TryGetValue(resourceType, out _publisher))
            {
                _publisher.OnNext(new CatalogChange(resourceType, resourceLocator, changeAction));
            }
        }

        private IObservable<CatalogChange> GetOrCreateCatalogPublisher(Type resourceType)
        {
            return _catalogListerners.GetOrAdd(resourceType, (t) => 
            {
                var _subject = new Subject<CatalogChange>();
                _compositeDisposable.Add(_subject);
                return _subject;
            });
        }

        private void RegisterSelfResources()
        {
            RegisterResource(typeof(IResourceCatalog), null, new StaticResourceLocator(this), true);
            RegisterResource(typeof(IResourceCatalogEx), null, new StaticResourceLocator(this), true);
            RegisterResource(typeof(ITypeBuilder), null, new StaticResourceLocator(_typeBuilder), true);
        }

#endregion

#region Helpers

        private void Dispose(bool disposing) 
        {
            if (disposing)
            {
                if (!_compositeDisposable.IsDisposed)
                {
                    _runtimeMapper.Dispose();
                    ((IDisposable)_typeBuilder).Dispose();
                    _compositeDisposable.Dispose();
                    _defaultResourceLocators.Clear();
                    _resourceLocators.Clear();
                    _knownTypes.Clear();
                    //_adapters.Clear(); <-- need to do this as well
                }
            }
        }

#endregion

#region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

#endregion

        ~ResourceCatalog() { Dispose(false); }

    }
}